// Ofri Keidar 302933981 group 05

package server;

import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;

import controller.DBController;
import controller.Message;

/**
 * Class controls server operations- creates the server socket and manages handling clients
 */
public class ServerController {

	// connection details
	private static final int PORT = 7000;
	private static final int SERVER_TIMEOUT = 1000; // how long server socket waits in accept()
	private static final int STOP_SERVER_INDICATOR = 1; // value entered by administrator user that stops the server
	private ServerSocket serverSocket;
	
	// object to communicate with DB system
	private ServerDBControllerAdapter adapter;

	// indicates if should keep listening to clients
	private AtomicBoolean keepRunning;
	
	/**
	 * Constructor- creates a new server controller object
	 * @param transactor- controller object for database-system
	 * @throws IOException if had problems creating the server socket
	 */
	public ServerController(DBController transactor) throws IOException {
			
		// keep running until stopped
		keepRunning = new AtomicBoolean(true);
		
		// set new adapter for the controller 
		adapter = new ServerDBControllerAdapter(transactor);

		// open server
		System.out.println("Starting server...");
		serverSocket = new ServerSocket(PORT);
		serverSocket.setSoTimeout(SERVER_TIMEOUT);
		System.out.println("Server is running!");
	}
	
	/**
	 * Method blocks program's flow until client writes data to socket,
	 * flow is blocked for SERVER_TIMEOUT mili-seconds.
	 * Once data is written to socket, the socket is returned
	 * @return socket holding client's input
	 * @throws IOException if had problems reading client's data and setting streams
	 */
	public Socket getClientData() {

		// wait for input data and return it
		try {
			return serverSocket.accept();
		} catch (IOException e) {}
				
		return null;
	}
	
	/**
	 * Gets object input stream received from socket holding client's input,
	 * and extracts Message object sent by client
	 * @param fromClientObjectStream- object input stream holding client's message
	 * @return Message object sent by client, or null if could not read message
	 */
	public Message getMessage(ObjectInputStream fromClientObjectStream) {
		
		try {

			// read Message object from client 
			Message fromServerMsg = (Message) fromClientObjectStream.readObject();
			
			// return read message
			return fromServerMsg;
			
		} catch (IOException e) {
			
			// could not read message
			return null;
			
		} catch (ClassNotFoundException e) {
			
			// could not read message
			return null;
		}
	}
	
	/**
	 * Uses the adapter to perform the query sent by client's Message object
	 * @param fromClientMsg- message sent from client
	 * @return Message object holding answer to the query
	 */
	public Message handleQuery(Message fromClientMsg) {
		
		// perform query and return answer message
		return adapter.handleClientQuery(fromClientMsg);
	}
	
	/**
	 * Sends to client given Message object through given object output stream
	 * @param toClientMsg- message to send
	 * @param toClientObjectStream- object output stream to client's socket
	 * @return True if could send message, False if not
	 */
	public boolean sendMessage(Message toClientMsg, ObjectOutputStream toClientObjectStream) {
		try {

			// send message to client
			toClientObjectStream.writeObject(toClientMsg);
			
			// message sent successfully
			return true;
			
		} catch (IOException e) {
			
			// could not send message
			return false;
		}
	}
	
	/**
	 * Closes server socket. 
	 * If server socket is null, returns False. Otherwise, returns True if could close the server
	 * @return True if could close server, False if reference to server is null
	 * @throws IOException if had problems closing the server
	 */
	public boolean closeServerSocket() throws IOException {
		
		// check if server is null
		if (serverSocket == null) {
			
			// cannot close server
			return false;
		}
			
		// try closing server
		serverSocket.close();
		
		// server closed successfully
		return true;
	}
	
	/**
	 * Closes given socket. 
	 * If socket is null, returns False. Otherwise, returns True if could close the socket
	 * @param socket- socket to close
	 * @return True if could close socket, False if reference to socket is null
	 * @throws IOException if had problems closing the socket
	 */
	public boolean closeSocket(Socket socket) {
		
		// check if socket is null
		if (socket == null) {
			
			// cannot close socket
			return false;
		}
			
		// try closing socket
		try {
			
			socket.close();
			
			// socket closed successfully
			return true;
			
		} catch (IOException e) {
			
			// could not close socket
			return false;
		}
		
	}
	
	/**
	 * Creates a new thread, where server administrator can ask to stop server's running.
	 * If STOP_SERVER_INDICATOR is entered, server is closed after done handling the last client
	 */
	public void startUserListener() {
		
		// create a new thread
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				
				// create scanner to read from console
				Scanner in = new Scanner(System.in);
				
				// stop server's running only if 1 is entered
				int input = 0;
				do {
					
					// ask for input
					System.out.println("Please enter " + STOP_SERVER_INDICATOR + " in order to stop server");
					input = in.nextInt();
					
				} while (input != STOP_SERVER_INDICATOR);
				
				// stop server's running
				keepRunning.set(false);				
				
				// close scanner
				in.close();
				
			}
		}).start();
	}
	
	/**
	 * Method runs until server is stopped and keeps listening to clients' input.
	 * When a client sends a message, a new thread is created, where message
	 * is read, dealt and an answer message is returned to the client.
	 * If had problems while reading from socket or sending answer message,
	 * socket is closed and ends thread
	 * @throws IOException if could not get client's input
	 */
	public void listenToClients() throws IOException {
		
		// allow administrator to stop server's running
		startUserListener();
		
		// keep running until stopped
		while (keepRunning.get()) {
			
			// wait until client sends a query
			final Socket socket = getClientData();
			
			if (socket == null) {
				
				// check if read input after time out
				continue;
			}
		
			// handle client's request
			handleClient(socket);			
		}
	}

	/**
	 * Handles client's request in a new thread:
	 * reads client's message, handles the query and sends an answer message
	 * @param socket- socket holding client's input data
	 */
	private void handleClient(final Socket socket) {
		
		// perform client's query in a new thread
		Runnable runnable = new Runnable() {

			// indicates if error when server handles client's request
			boolean serverError = false;
			
			@Override
			public void run() {
				
				do {
					
					// create a new SocketData object with given socket
					SocketData currSocketData = null;
					try {			
						currSocketData = new SocketData(socket);
						
					} catch (EOFException e) {
							
						// client has closed socket
						serverError = true;
						continue;
						
					} catch (IOException e) {
	
						// could not connect to socket
						serverError = true;
						continue;
					} 
	
					// get message from client
					Message fromClientMsg = getMessage(currSocketData.getFromClientObjectStream());
	
					// check if could get message from client
					if (fromClientMsg == null) {						
	
						// close socket to current client
						closeSocket(currSocketData.getSocket());
	
						// end thread
						serverError = true;
						continue;
					}
	
					// compose answer message
					Message toClientMsg = handleQuery(fromClientMsg);
	
					// check if could get answer message
					if (toClientMsg == null) {						
	
						// close socket to current client
						closeSocket(currSocketData.getSocket());
	
						// end thread
						serverError = true;
						continue;
					}
	
					// send answer message to client
					boolean couldSendMsg = sendMessage(toClientMsg, currSocketData.getToClientObjectStream());
	
					// check if could send message
					if (!couldSendMsg) {
	
						// close socket to current client
						closeSocket(currSocketData.getSocket());
						
						// end thread
						serverError = true;
						continue;
					}
					
				} while (socket.isConnected() && !serverError); 
			}
		};
		
		// start new thread's running
		new Thread(runnable).start();
	}
}